# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: inkyu

import collections
import logging

PerProductBookCheckerStatus = collections.namedtuple('PerProductBookCheckerStatus',
                                                     [
                                                         'book_valid',
                                                         'book_count',
                                                         'last_book_received_sec',
                                                         'last_book_changed_sec',
                                                     ])

EachSideStat = collections.namedtuple('EachSideStat', ['depth', 'bbo_price', 'bbo_qty'])


class PerProductBookChecker(object):
  def __init__(self, *, product, config, timestamp, logger=None):
    self._product = product
    self._config = config
    self._initialized_at = timestamp

    self._last_received_at = None
    self._last_changed_at = None
    self._last_valid_at = None
    self._last_ask_stat = None
    self._last_bid_stat = None
    self._count = 0

    # TODO(jshin): Parameterized
    self._valid_book_depth = 3
    self._max_spread_ratio = 0.05

    self._valid = True
    self._logger = logger or logging.getLogger(__name__)

  @property
  def valid(self):
    return self._valid

  @property
  def count(self):
    return self._count

  def _check_timestamp(self, *, timestamp):
    # No feed
    if self._last_received_at is None:
      last_received_before = timestamp - self._initialized_at
    else:
      last_received_before = timestamp - self._last_received_at
    if self._config.max_book_gap < last_received_before < 1e14:
      self._logger.warning('%s: No feed in %.2f seconds (max_book_gap = %s seconds)',
                           self._product,
                           last_received_before / 1e9,
                           self._config.max_book_gap / 1e9)
      return False
    # No change
    if self._last_changed_at is None:
      last_changed_before = timestamp - self._initialized_at
    else:
      last_changed_before = timestamp - self._last_changed_at
    if self._config.max_book_change_gap < last_changed_before < 1e14:
      self._logger.warning('%s: No book change in %s seconds',
                           self._product,
                           last_changed_before / 1e9)
      return False
    return True

  def _check_validity(self, *, ask_stat, bid_stat):
    # Book
    if bid_stat.depth < self._valid_book_depth:
      self._logger.debug('%s: Shallow bid book: %d < %d',
                         self._product,
                         bid_stat.depth,
                         self._valid_book_depth)
      return False
    if ask_stat.depth < self._valid_book_depth:
      self._logger.debug('%s: Shallow ask book: %d < %d',
                         self._product,
                         ask_stat.depth,
                         self._valid_book_depth)
      return False
    if ask_stat.bbo_price <= bid_stat.bbo_price:
      self._logger.warning('%s: Book crossed: %d <= %d',
                           self._product,
                           ask_stat.bbo_price,
                           bid_stat.bbo_price)
      return False
    spread_ratio = (ask_stat.bbo_price - bid_stat.bbo_price) / bid_stat.bbo_price
    if spread_ratio > self._max_spread_ratio:
      self._logger.debug('%s: Too wide spread: %d ~ %d',
                         self._product,
                         bid_stat.bbo_price,
                         ask_stat.bbo_price)
      return False
    return True

  def update_timestamp(self, *, timestamp):
    if not self._valid:
      return
    if not self._check_timestamp(timestamp=timestamp):
      self._valid = False
      return
    self._last_valid_at = timestamp

  def on_book(self, *, book, timestamp):
    if not self._valid:
      return

    if not self._check_timestamp(timestamp=timestamp):
      self._valid = False
      return

    ask0 = book.ask0()
    bid0 = book.bid0()
    if not all((ask0, bid0)):
      self._logger.warning('%s: No ask/bid book', self._product)
      self._valid = False
      return

    ask_stat = EachSideStat(depth=book.depth_ask(), bbo_price=ask0.price, bbo_qty=ask0.qty)
    bid_stat = EachSideStat(depth=book.depth_bid(), bbo_price=bid0.price, bbo_qty=bid0.qty)
    if not self._check_validity(ask_stat=ask_stat, bid_stat=bid_stat):
      self._valid = False
      return

    self._last_valid_at = timestamp
    self._last_received_at = timestamp
    if self._last_ask_stat is not None and self._last_ask_stat != ask_stat:
      self._last_changed_at = timestamp
    if self._last_bid_stat is not None and self._last_bid_stat != bid_stat:
      self._last_changed_at = timestamp
    self._last_ask_stat = ask_stat
    self._last_bid_stat = bid_stat
    self._count += 1

  def get_status(self, *, timestamp):
    return PerProductBookCheckerStatus(
        book_valid=self._valid,
        book_count=self._count,
        last_book_received_sec=(timestamp - self._last_received_at)
        * 1e-9 if self._last_received_at else None,
        last_book_changed_sec=(timestamp - self._last_changed_at)
        * 1e-9 if self._last_changed_at else None,
    )
